package com.capgemini.cn.authority.rest.service;

import com.capgemini.cn.authority.data.core.BaseEntity;
import com.capgemini.cn.authority.rest.exception.AuthRestBusinessException;
import com.capgemini.cn.authority.rest.exception.IntegrityConstraintException;
import com.capgemini.cn.authority.rest.valueobject.DataPage;
import com.capgemini.cn.authority.rest.valueobject.DataResponse;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.exception.ConstraintViolationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Created at 2017/9/18
 *
 * @param <D> 需要返回给客户端的值对象类型
 * @param <E> 数据库对应的实体对象类型
 * @author Andriy
 */
public abstract class AbstractBaseService<D extends E, E> implements CommonService<D> {

    private final Logger logger = LoggerFactory.getLogger(AbstractBaseService.class);

    @Autowired
    @Getter(value = AccessLevel.PROTECTED)
    @Setter
    // TODO 因为用了泛型所以可能导致无法自动注入
    private JpaRepository<E, Long> repository;

    /**
     * <p>将实体对象转换成值对象</p>
     * <p><span style='color:yellow'>但是值对象必须是实体对象的子类，否则无法成功转换</span></p>
     *
     * @param entity           待转换的实体对象
     * @param valueObjectClass 需要转换成结果的值对象类型
     * @param ignoreProperties 不需要处理的属性名
     * @return 返回指定的值对象实体
     */
    protected final D conversionToValueObject(Object entity, Class<D> valueObjectClass, String... ignoreProperties) {
        D result = null;
        try {
            result = valueObjectClass.newInstance();
            // TODO 在确保值对象与实体对象存在继承关系之后开始直接复制实体对象中的属性值到值对象中
//            if (result.getClass().getSuperclass().equals(entity.getClass()))
            BeanUtils.copyProperties(entity, result, ignoreProperties);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * <p>将实体对象转换成值对象</p>
     * <p><span style='color:yellow'>但是值对象必须是实体对象的子类，否则无法成功转换</span></p>
     *
     * @param entities         待转换的实体对象集合
     * @param valueObjectClass 需要转换成结果的值对象类型
     * @param ignoreProperties 不需要处理的属性名
     * @return 返回指定的值对象实体集合
     */
    protected final List<D> conversionToValueObject(List<E> entities, Class<D> valueObjectClass, String... ignoreProperties) {
        List<D> resultList = new ArrayList<>();
        for (E entity : entities)
            resultList.add(this.conversionToValueObject(entity, valueObjectClass));
        return resultList;
    }

    @Override
    public abstract DataResponse<List<D>> findAll(int page, int size, String orderProp, Sort.Direction direction);

    /**
     * 分页查询数据列表并对指定属性按规则排序
     *
     * @param valueObjectClass 值对象类型，应该与当前抽象类上的泛型{@link D}保持一致
     * @param page             查询的页码数，默认为0（即从第一页开始查询）
     * @param size             每页数据的条数，如果值为-1或0则表示查询所有的数据（即最大条数，此时分页参数失效）
     * @param orderProp        排序属性名称
     * @param direction        排序规则
     * @param ignoreProperties 实体对象转值对象时不需要处理的属性名称
     * @return 返回通用响应对象并以集合存储值对象
     */
    protected final DataResponse<List<D>> findAll(Class<D> valueObjectClass, int page, int size, String orderProp, Sort.Direction direction, String... ignoreProperties) {
        DataResponse<List<D>> result = new DataResponse<>();

        Page<E> pageEntities;
        List<E> entities;
        // 只有size参数有效时才会进行分页查询
        if (size > 0) {
            pageEntities = this.repository.findAll(new PageRequest(page, size, direction, orderProp));
            entities = pageEntities.getContent();
            result.setPages(new DataPage(pageEntities.getTotalPages(), pageEntities.getTotalElements(), pageEntities.getNumber(), pageEntities.getSize(), pageEntities.isFirst(), pageEntities.isLast()));
        } else {
            entities = this.repository.findAll(new Sort(direction, orderProp));
        }
        List<D> resultList = new ArrayList<>();
        for (E entity : entities)
            resultList.add(this.conversionToValueObject(entity, valueObjectClass, ignoreProperties));
        result.setResponse(resultList);
        return result;
    }

    @Override
    public abstract DataResponse<D> findById(long id);

    /**
     * 根据ID查询单个数据实体对象{@link D}
     *
     * @param valueObjectClass 值对象类型，应该与当前抽象类上的泛型{@link D}保持一致
     * @param id               数据ID主键值
     * @param ignoreProperties 实体对象转值对象时不需要处理的属性名称
     * @return 包装成{@link DataResponse}对象返回对应的实体对象数据
     */
    protected final DataResponse<D> findById(Class<D> valueObjectClass, long id, String... ignoreProperties) {
        E entity = this.repository.getOne(id);
        if (ObjectUtils.isEmpty(entity)) throw new AuthRestBusinessException("无法根据主键ID（" + id + "）查询到对应的实体对象信息！");
        return new DataResponse<D>().setResponse(this.conversionToValueObject(entity, valueObjectClass, ignoreProperties));
    }

    @Override
    public abstract DataResponse<D> add(D source);

    @Override
    public abstract boolean add(List<D> sourceList);

    @Override
    public abstract DataResponse<D> update(long id, Map<String, Object> properties) throws AuthRestBusinessException;

    /**
     * 更新指定属性值
     *
     * @param valueObjectClass 值对象类型，应该与当前抽象类上的泛型{@link D}保持一致
     * @param id               数据ID主键值
     * @param properties       需要更新的属性列表，key为字符串类型用以标识需要更新的属性名称，value为对象类型用以存放更新后的数据值
     * @param ignoreProperties 实体对象转值对象时不需要处理的属性名称
     * @return 包装成{@link DataResponse}对象返回对应的实体对象数据
     */
    protected final DataResponse<D> update(Class<D> valueObjectClass, long id, Map<String, Object> properties, String... ignoreProperties) throws AuthRestBusinessException {
        E entity = this.getRepository().getOne(id);
        if (ObjectUtils.isEmpty(entity)) throw new AuthRestBusinessException("无法根据主键ID（" + id + "）查询到对应的实体对象信息！");
        // 将需要更新的属性设置到实体对象中
        this.copyProperties(entity, valueObjectClass, properties);
        // 设置更新时间
        ((BaseEntity) entity).setUpdateTime(new Timestamp(System.currentTimeMillis()));
        // 执行更新操作
        E updatedEntity = this.getRepository().saveAndFlush(entity);
        // 将实体对象转换成值对象并返回
        return new DataResponse<D>().setResponse(this.conversionToValueObject(updatedEntity, valueObjectClass, ignoreProperties));
    }

    /**
     * 将值对象中指定的属性值复制到实体对象中
     *
     * @param entity           存放值的实体对象
     * @param valueObjectClass 获取值的值对象
     * @param properties       需要操作的属性集合
     */
    protected void copyProperties(E entity, Class<D> valueObjectClass, Map<String, Object> properties) {
        // 遍历需要更新的属性集合并设置到对应的实体对象中
        for (String prop : properties.keySet()) {
            try {
                PropertyDescriptor propertyDescriptor = BeanUtils.getPropertyDescriptor(valueObjectClass, prop);
                propertyDescriptor.getWriteMethod().invoke(entity, properties.get(prop));
            } catch (InvocationTargetException e) {
                throw new AuthRestBusinessException("无法在数据实体对象中查找到" + prop + "属性！", e);
            } catch (IllegalAccessException e) {
                throw new AuthRestBusinessException("无法对属性" + prop + "进行设置值操作！", e);
            }
        }
    }


    @Override
    public abstract boolean update(Map<String, Object> properties, List<D> sourceList);

    @Transactional
    @Override
    public boolean delete(long id) throws AuthRestBusinessException {
        E entity = this.getRepository().getOne(id);
        if (ObjectUtils.isEmpty(entity)) throw new AuthRestBusinessException("无法根据主键ID（" + id + "）查询到对应的实体对象信息！");
        try {
            this.repository.delete(entity);
        } catch (DataIntegrityViolationException e) {
            if (e.getCause() instanceof ConstraintViolationException) {
                final String exceptionMsg = "根据主键ID（" + id + "）删除数据时发现有外键关系约束，无法执行删除！";
                this.logger.warn(exceptionMsg);
                throw new IntegrityConstraintException(exceptionMsg);
            } else {
                throw e;
            }
        }
        return true;
    }

    @Transactional
    @Override
    public boolean delete(List<Long> ids) {
        List<E> entities = this.repository.findAll(ids);
        this.repository.deleteInBatch(entities);
        return true;
    }
}
